// SPDX-License-Identifier: GPL-2.0
/*
 * lombo_mtdnand.c - lombo mtdnand driver code for LomboTech
 * lombo mtdnand driver driver code implement
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>

/* #define DBG */
#include <mach/debug.h>
#undef MOD_NAME
#define MOD_NAME	"LOMBO_MTDNAND"

#include "include/nand_phy.h"
#include "lombo_mtdnand_if.h"
#include "common.h"

#define LOMBO_MTDNAND_DEBUG
#ifdef LOMBO_MTDNAND_DEBUG
#define DBG(fmt, ...)	PRT_INFO(fmt "\n", ##__VA_ARGS__)
#endif

#define ERR(fmt, ...)	PRT_ERR(fmt "\n", ##__VA_ARGS__)

struct lombo_mtdnand_param {
	unsigned int page_size;
	unsigned int oob_size;
	unsigned int block_size;
	unsigned int block_cnt;
};

struct lombo_mtdnand_operations {
	int (*nand_read_oob)(loff_t from, struct mtd_oob_ops *ops);
	int (*nand_write_oob)(loff_t to, struct mtd_oob_ops *ops);
	int (*nand_erase)(unsigned int block);
	int (*nand_sync)(void);
};

struct lombo_mtdnand {
	struct mtd_info mtd;
	struct lombo_mtdnand_param param;
	struct lombo_mtdnand_operations *ops;
};

static const char * const part_types[] = { "cmdlinepart", NULL };
static struct lombo_mtdnand *g_lombo_mtdnand = NULL;

static struct lombo_mtdnand_operations lombo_mtdnand_ops = {
	.nand_read_oob = _lombo_nand_read_oob,
	.nand_write_oob = _lombo_nand_write_oob,
	.nand_erase = _lombo_nand_erase,
	.nand_sync = _lombo_nand_sync,
};

static void lombo_mtdnand_param_init(struct lombo_mtdnand_param *param)
{
	param->page_size = _lombo_nand_get_page_size();
	param->oob_size = _lombo_nand_get_oob_size();
	param->block_size = _lombo_nand_get_block_size();
	param->block_cnt = _lombo_nand_get_block_cnt();
}

static inline struct lombo_mtdnand *get_lombo_mtdnand(void)
{
	return g_lombo_mtdnand;
}

static inline int lombo_nand_read_oob(loff_t from, struct mtd_oob_ops *ops)
{
	return get_lombo_mtdnand()->ops->nand_read_oob(from, ops);
}

static inline int lombo_nand_write_oob(loff_t to, struct mtd_oob_ops *ops)
{
	return get_lombo_mtdnand()->ops->nand_write_oob(to, ops);
}

static inline int lombo_nand_erase(unsigned int block)
{
	return get_lombo_mtdnand()->ops->nand_erase(block);
}

static inline int lombo_nand_sync(void)
{
	return get_lombo_mtdnand()->ops->nand_sync();
}

/*
 * mtd_read_oob will call mtd_check_oob_ops to check mtd_oob_ops
 * mtd_read_oob init ops->retlen = ops->oobretlen = 0
 */
static int lombo_mtdnand_read_oob(struct mtd_info *mtd, loff_t from,
			struct mtd_oob_ops *ops)
{
	int ret;

	spnd_common_mutex_lock();
	ret = lombo_nand_read_oob(from, ops);
	spnd_common_mutex_unlock();

	if (!ret)
		return 0;
	else {
		ERR("lombo_nand_read_oob fail(%d)", ret);
		return -EIO;
	}
}

/*
 * mtd_write_oob will call mtd_check_oob_ops to check mtd_oob_ops
 * mtd_write_oob init ops->retlen = ops->oobretlen = 0
 */
static int lombo_mtdnand_write_oob(struct mtd_info *mtd, loff_t to,
			struct mtd_oob_ops *ops)
{
	int ret;

	spnd_common_mutex_lock();
	ret = lombo_nand_write_oob(to, ops);
	spnd_common_mutex_unlock();

	if (!ret)
		return 0;
	else {
		ERR("lombo_nand_write_oob fail(%d)", ret);
		return -EIO;
	}
}

/*
 * mtd_erase will check instr->addr and instr->len
 */
static int lombo_mtdnand_erase(struct mtd_info *mtd, struct erase_info *instr)
{
	unsigned int block_addr;
	unsigned int block_cnt;

	/* Start address must align on block boundary */
	if (instr->addr & mtd->erasesize_mask) {
		ERR("Unaligned address");
		return -EINVAL;
	}

	/* Length must align on block boundary */
	if (instr->len & mtd->erasesize_mask) {
		ERR("Length not block aligned");
		return -EINVAL;
	}

	block_addr = instr->addr >> mtd->erasesize_shift;
	block_cnt = instr->len >> mtd->erasesize_shift;

	spnd_common_mutex_lock();
	while(block_cnt > 0) {
		int ret = lombo_nand_erase(block_addr);
		if (ret) {
			spnd_common_mutex_unlock();
			ERR("lombo_nand_erase block(%d) fail(%d)",
				block_addr, ret);
			return -EIO;
		}

		block_addr++;
		block_cnt--;
	}
	spnd_common_mutex_unlock();

	return 0;
}

static void lombo_mtdnand_sync(struct mtd_info *mtd)
{
	spnd_common_mutex_lock();
	lombo_nand_sync();
	spnd_common_mutex_unlock();
}

/* return val: 0 -> good block; not 0 -> bad block */
static int lombo_mtdnand_block_isbad(struct mtd_info *mtd, loff_t ofs)
{
	return 0;
}

static int lombo_mtdnand_block_markbad(struct mtd_info *mtd, loff_t ofs)
{
	return 0;
}

int lombo_mtdnand_init(void)
{
	struct mtd_info *mtd;
	struct lombo_mtdnand_param *param;
	struct mtd_part_parser_data ppdata;

	if (g_lombo_mtdnand) {
		DBG("lombo_mtdnand has been registered");
		return 0;
	}

	g_lombo_mtdnand = kzalloc(sizeof(*g_lombo_mtdnand), GFP_KERNEL);
	if (!g_lombo_mtdnand) {
		ERR("alloc lombo_mtdnand fail");
		return -ENOMEM;
	}

	mtd = &g_lombo_mtdnand->mtd;
	param = &g_lombo_mtdnand->param;

	lombo_mtdnand_param_init(param);
	g_lombo_mtdnand->ops = &lombo_mtdnand_ops;

	mtd->writesize = param->page_size;
	mtd->writebufsize = param->page_size;
	mtd->oobsize  = param->oob_size;
	mtd->oobavail  = param->oob_size;
	mtd->erasesize = param->block_size;
	mtd->size = (uint64_t)(param->block_size) * (uint64_t)(param->block_cnt);

	/* Fill in remaining MTD driver data */
	mtd->type = MTD_NANDFLASH;
	mtd->flags = MTD_CAP_NANDFLASH;
	mtd->owner = THIS_MODULE;

	mtd->_erase = lombo_mtdnand_erase;
	mtd->_read_oob = lombo_mtdnand_read_oob;
	mtd->_write_oob = lombo_mtdnand_write_oob;
	mtd->_sync = lombo_mtdnand_sync;
	mtd->_block_isbad = lombo_mtdnand_block_isbad;
	mtd->_block_markbad = lombo_mtdnand_block_markbad;

	mtd->name = "lombo-spnd";

	/*
	 * when there are partitions, master device is not added,
	 * and erasesize_mask/erasesize_shift are not caculated.
	 */
	mtd->erasesize_shift = ffs(mtd->erasesize) - 1;
	mtd->erasesize_mask = mtd->erasesize - 1;

	if (mtd_device_parse_register(mtd, part_types, &ppdata, NULL, 0)) {
		ERR("mtd_device_parse_register fail");
		kfree(g_lombo_mtdnand);
		g_lombo_mtdnand = NULL;
		return -EINVAL;
	}

	return 0;
}

void lombo_mtdnand_exit(void)
{
	if (g_lombo_mtdnand) {
		mtd_device_unregister(&g_lombo_mtdnand->mtd);
		kfree(g_lombo_mtdnand);
		g_lombo_mtdnand = NULL;
	}
}

